Java 锁

Java高效并发中有介绍相关的概念,但是不是很系统,这里需要系统的整理回顾一下

锁的位置

锁存在于Java对象头中,对象头中有一个Mark Word的数据区域,是和锁相关的。markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容

  • 无锁状态

最后的两个的标志位是01,存储内容是对象的hashCode,分带年龄

  • 轻量级锁

最后的两个的标志位是00,存储内容是指向栈中锁记录的指针

  • 重量级锁

最后的两个的标志位是10,存储内容是指向互斥量(重量级锁)指针

  • GC标记

最后的两个的标志位是11,不存任何数据,准备被回收了

  • 偏向锁

最后的两个的标志位是01,和无锁的是一样的,但是它的倒数第三位bit是1,表示是偏向锁,存储内容是偏向线程ID、偏向时间戳、对象分代年龄

锁的升级

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

偏向锁和轻量级锁属于乐观锁,使用CAS来操作对象头的数据;重量级锁属于悲观锁

  1. 偏向锁适合只有一个线程多次访问该对象的情况

  2. 轻量级锁适合多个线程轮流进入临界区的情况

  3. 重量级锁适合多个线程同时试图进入临界区的情况

偏向锁和轻量级锁并不能解决并发冲突的情况。

偏向锁

为什么要引入偏向锁呢?大多数情况,对象不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁

偏向的意思是:这个对象偏向于某个线程

它的实现过程如下:

获取偏向锁

  1. 线程要获取某个对象的锁时,访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态

  2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。

  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。

  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)

  5. 执行同步代码。

释放偏向锁

偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

1
2
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking

轻量级锁

轻量级锁是由偏向所升级来的,在一个线程进入同步块的情况下(该线程占有偏向锁),当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;

轻量级锁的加锁过程

  1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。

  2. 拷贝对象头中的Mark Word复制到锁记录中

  3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。

  4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。

  5. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

轻量级锁的释放

由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的markword,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对markword做了修改,两者比对发现不一致,则切换到重量锁。

等待轻量锁的线程不会阻塞,它会一直自旋等待锁,并如上所说修改markword。

锁何时升级

偏向锁升级成轻量级锁

当线程1占有某对象的偏向锁时,线程2尝试获取锁,发现该对象是偏向锁,此时判断占用该偏向锁的线程是否存在,如果不存在,则取代它,mark word设置为线程2,否则,系统暂定线程1,设置轻量级锁的状态,在线程1栈中分配Lock Record空间,拷贝Mark Word,设置对象头中的owner,正式升级锁为轻量级锁,打开线程1继续运行,线程2自旋等待。

轻量级锁升级为重量级锁

当线程1占有某对象的轻量级锁时,线程3尝试过去锁